package analyzer

import (
	"fmt"
	"strings"
	"sync"

	malysisv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/malysis/v1"
	"github.com/safedep/vet/gen/checks"
	"github.com/safedep/vet/gen/filtersuite"
	"github.com/safedep/vet/pkg/common/logger"
	"github.com/safedep/vet/pkg/models"
	"github.com/safedep/vet/pkg/readers"
)

type MalwareAnalyzerConfig struct {
	// Flag to trust automated analysis results without needing
	// a verification record
	TrustAutomatedAnalysis bool

	// Fail fast on malware detection
	FailFast bool

	// Minimum confidence level for malicious package analysis result to fail fast
	// Should be HIGH, MEDIUM or LOW
	MinimumConfidence string

	// Internally mapped confidence level as per protobuf spec
	minimumConfidenceLevel malysisv1.Report_Evidence_Confidence
}

func DefaultMalwareAnalyzerConfig() MalwareAnalyzerConfig {
	return MalwareAnalyzerConfig{
		TrustAutomatedAnalysis: false,
		FailFast:               true,
	}
}

type malwareAnalyzer struct {
	config MalwareAnalyzerConfig
	m      sync.Mutex
}

var _ Analyzer = (*malwareAnalyzer)(nil)

func NewMalwareAnalyzer(config MalwareAnalyzerConfig) (*malwareAnalyzer, error) {
	config.minimumConfidenceLevel = malysisv1.Report_Evidence_CONFIDENCE_HIGH
	if config.MinimumConfidence != "" {
		confidenceName := fmt.Sprintf("CONFIDENCE_%s", strings.ToUpper(config.MinimumConfidence))
		conf, ok := malysisv1.Report_Evidence_Confidence_value[confidenceName]
		if !ok {
			return nil, fmt.Errorf("invalid minimum confidence level: %s", config.MinimumConfidence)
		}

		config.minimumConfidenceLevel = malysisv1.Report_Evidence_Confidence(conf)
	}

	return &malwareAnalyzer{
		config: config,
	}, nil
}

func (a *malwareAnalyzer) Name() string {
	return "Malware Analyzer"
}

func (a *malwareAnalyzer) Analyze(manifest *models.PackageManifest,
	handler AnalyzerEventHandler,
) error {
	return readers.NewManifestModelReader(manifest).EnumPackages(func(pkg *models.Package) error {
		err := a.applyMalwareDecision(pkg)
		if err != nil {
			return fmt.Errorf("MalwareAnalyzer: Failed to apply malware decision for package %s/%s/%s: %w",
				pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion(), err)
		}

		if !pkg.IsMalware() && !pkg.IsSuspicious() {
			return nil // No action needed if not malware or suspicious
		}

		if pkg.IsMalware() {
			filterMsg := fmt.Sprintf("MalwareAnalyzer: Package %s/%s/%s is classified as malicious",
				pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion())

			// Trigger a policy violation event so that it gets recorded
			// across reports that are tracking policy violations
			err = handler(&AnalyzerEvent{
				Type:     ET_FilterExpressionMatched,
				Source:   a.Name(),
				Manifest: manifest,
				Package:  pkg,
				Message:  filterMsg,
				Filter: &filtersuite.Filter{
					Name:        "malware",
					CheckType:   checks.CheckType_CheckTypeMalware,
					Summary:     fmt.Sprintf("Malicious (malware) component detected by %s", a.Name()),
					Description: fmt.Sprintf("%s analyzed the package for possible malicious behavior", a.Name()),
					References:  []string{"https://docs.safedep.io/cloud/malware-analysis"},
					Tags:        []string{"malware-analysis"},
				},
			})
		} else if pkg.IsSuspicious() {
			filterMsg := fmt.Sprintf("MalwareAnalyzer: Package %s/%s/%s is suspicious package",
				pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion())

			// Trigger a policy violation event so that it gets recorded
			// across reports that are tracking policy violations
			err = handler(&AnalyzerEvent{
				Type:     ET_SuspiciousPackage,
				Source:   a.Name(),
				Manifest: manifest,
				Package:  pkg,
				Message:  filterMsg,
				Filter: &filtersuite.Filter{
					Name:        "suspicious",
					CheckType:   checks.CheckType_CheckTypeMalware,
					Summary:     fmt.Sprintf("Suspicious (malware) component detected by %s", a.Name()),
					Description: fmt.Sprintf("%s analyzed the package for possible suspicious behavior", a.Name()),
					References:  []string{"https://docs.safedep.io/cloud/malware-analysis"},
					Tags:        []string{"malware-analysis"},
				},
			})
		}

		if err != nil {
			logger.Errorf("MalwareAnalyzer: Failed to handle filter event for package %s/%s/%s: %v",
				pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion(), err)
		}

		if a.config.FailFast && pkg.IsMalware() {
			return handler(&AnalyzerEvent{
				Type:   ET_AnalyzerFailOnError,
				Source: a.Name(),
				Err: fmt.Errorf("malware detected in package %s/%s/%s",
					pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion()),
			})
		}

		return nil
	})
}

func (a *malwareAnalyzer) Finish() error {
	return nil
}

// The malware analyzer makes a decision based on configuration and malware analysis results.
// The decision involves:
//
// - No action if the package is not classified as malware
// - Malware if a verification record is available to confirm
// - Malware if `TrustAutomatedAnalysis` config is enabled and confidence is high
// - Suspicious for all other cases
func (a *malwareAnalyzer) applyMalwareDecision(pkg *models.Package) error {
	ma := pkg.GetMalwareAnalysisResult()
	if ma == nil || ma.Report == nil {
		logger.Warnf("MalwareAnalyzer: No malware analysis result found for package %s/%s/%s",
			pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion())
		return nil
	}

	a.m.Lock()
	defer a.m.Unlock()

	report := ma.Report
	if !report.GetInference().GetIsMalware() {
		return nil
	}

	vr := ma.VerificationRecord
	if vr != nil && vr.GetIsMalware() {
		logger.Warnf("MalwareAnalyzer: Package %s/%s/%s is classified as malwars with verification record",
			pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion())
		ma.IsMalware = true
		return nil
	}

	// By default we do not trust results without a verification record
	// unless the config is set to trust automated analysis and a minimum confidence is set
	if a.config.TrustAutomatedAnalysis && a.hasMinimumConfidence(report) {
		logger.Warnf("MalwareAnalyzer: Package %s/%s/%s is classified as malware",
			pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion())
		ma.IsMalware = true
		return nil
	}

	logger.Warnf("MalwareAnalyzer: Package %s/%s/%s is classified as suspicious due to low confidence",
		pkg.GetControlTowerSpecEcosystem(), pkg.GetName(), pkg.GetVersion())

	ma.IsSuspicious = true
	return nil
}

func (a *malwareAnalyzer) hasMinimumConfidence(report *malysisv1.Report) bool {
	confidence := report.GetInference().GetConfidence()
	if confidence == malysisv1.Report_Evidence_CONFIDENCE_UNSPECIFIED {
		return false
	}

	// Confidence is a protobuf enum, so we need to compare the integer values
	return confidence <= a.config.minimumConfidenceLevel
}
